# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Unit tests for parser.py."""

import unittest

from blinkpy.common.system.log_testing import LoggingTestCase
from blinkpy.style.optparser import ArgumentParser
from blinkpy.style.optparser import ArgumentPrinter
from blinkpy.style.optparser import CommandOptionValues as ProcessorOptions
from blinkpy.style.optparser import DefaultCommandOptionValues


class ArgumentPrinterTest(unittest.TestCase):
    """Tests the ArgumentPrinter class."""

    _printer = ArgumentPrinter()

    def _create_options(self,
                        output_format='emacs',
                        min_confidence=3,
                        filter_rules=None,
                        git_commit=None):
        return ProcessorOptions(
            filter_rules=filter_rules,
            git_commit=git_commit,
            min_confidence=min_confidence,
            output_format=output_format)

    def test_to_flag_string(self):
        options = self._create_options('vs7', 5, ['+foo', '-bar'], 'git')
        self.assertEqual(
            '--filter=+foo,-bar --git-commit=git '
            '--min-confidence=5 --output=vs7',
            self._printer.to_flag_string(options))

        # This is to check that --filter and --git-commit do not
        # show up when not user-specified.
        options = self._create_options()
        self.assertEqual('--min-confidence=3 --output=emacs',
                         self._printer.to_flag_string(options))


class ArgumentParserTest(LoggingTestCase):
    """Test the ArgumentParser class."""

    class _MockStdErr(object):
        def write(self, _):
            # We do not want the usage string or style categories
            # to print during unit tests, so print nothing.
            return

    def _parse(self, args):
        """Call a test parser.parse()."""
        parser = self._create_parser()
        return parser.parse(args)

    def _create_defaults(self):
        """Return a DefaultCommandOptionValues instance for testing."""
        return DefaultCommandOptionValues(
            min_confidence=3, output_format='vs7')

    def _create_parser(self):
        """Return an ArgumentParser instance for testing."""
        default_options = self._create_defaults()

        all_categories = ['build', 'whitespace']

        mock_stderr = self._MockStdErr()

        return ArgumentParser(
            all_categories=all_categories,
            base_filter_rules=[],
            default_options=default_options,
            mock_stderr=mock_stderr,
            usage='test usage')

    def test_parse_documentation(self):
        parse = self._parse

        # FIXME: Test both the printing of the usage string and the
        #        filter categories help.

        # Request the usage string.
        with self.assertRaises(SystemExit):
            parse(['--help'])
        # Request default filter rules and available style categories.
        with self.assertRaises(SystemExit):
            parse(['--filter='])

    def test_parse_bad_values(self):
        parse = self._parse

        # Pass an unsupported argument.
        with self.assertRaises(SystemExit):
            parse(['--bad'])
        self.assertLog(['ERROR: no such option: --bad\n'])

        with self.assertRaises(SystemExit):
            parse(['--min-confidence=bad'])
        self.assertLog([
            'ERROR: option --min-confidence: '
            "invalid integer value: 'bad'\n"
        ])
        with self.assertRaises(SystemExit):
            parse(['--min-confidence=0'])
        self.assertLog([
            'ERROR: option --min-confidence: invalid integer: 0: '
            'value must be between 1 and 5\n'
        ])
        with self.assertRaises(SystemExit):
            parse(['--min-confidence=6'])
        self.assertLog([
            'ERROR: option --min-confidence: invalid integer: 6: '
            'value must be between 1 and 5\n'
        ])
        parse(['--min-confidence=1'])  # works
        parse(['--min-confidence=5'])  # works

        with self.assertRaises(SystemExit):
            parse(['--output=bad'])
        self.assertLog([
            'ERROR: option --output-format: invalid choice: '
            "'bad' (choose from 'emacs', 'vs7')\n"
        ])
        parse(['--output=vs7'])  # works

        # Pass a filter rule not beginning with + or -.
        with self.assertRaises(SystemExit):
            parse(['--filter=build'])
        self.assertLog([
            'ERROR: Invalid filter rule "build": '
            'every rule must start with + or -.\n'
        ])
        parse(['--filter=+build'])  # works

    def test_parse_default_arguments(self):
        parse = self._parse

        (files, options) = parse([])

        self.assertEqual(files, [])

        self.assertEqual(options.filter_rules, [])
        self.assertIsNone(options.git_commit)
        self.assertFalse(options.diff_files)
        self.assertFalse(options.is_verbose)
        self.assertEqual(options.min_confidence, 3)
        self.assertEqual(options.output_format, 'vs7')

    def test_parse_explicit_arguments(self):
        parse = self._parse

        # Pass non-default explicit values.
        _, options = parse(['--min-confidence=4'])
        self.assertEqual(options.min_confidence, 4)
        _, options = parse(['--output=emacs'])
        self.assertEqual(options.output_format, 'emacs')
        _, options = parse(['-g', 'commit'])
        self.assertEqual(options.git_commit, 'commit')
        _, options = parse(['--git-commit=commit'])
        self.assertEqual(options.git_commit, 'commit')
        _, options = parse(['--git-diff=commit'])
        self.assertEqual(options.git_commit, 'commit')
        _, options = parse(['--verbose'])
        self.assertTrue(options.is_verbose)
        _, options = parse(['--diff-files', 'file.txt'])
        self.assertTrue(options.diff_files)

        # Pass user_rules.
        _, options = parse(['--filter=+build,-whitespace'])
        self.assertEqual(options.filter_rules, ['+build', '-whitespace'])

        # Pass spurious white space in user rules.
        _, options = parse(['--filter=+build, -whitespace'])
        self.assertEqual(options.filter_rules, ['+build', '-whitespace'])

    def test_parse_files(self):
        parse = self._parse

        files, _ = parse(['foo.cpp'])
        self.assertEqual(files, ['foo.cpp'])

        # Pass multiple files.
        files, _ = parse(['--output=emacs', 'foo.cpp', 'bar.cpp'])
        self.assertEqual(files, ['foo.cpp', 'bar.cpp'])


class CommandOptionValuesTest(unittest.TestCase):
    """Tests CommandOptionValues class."""

    def test_init(self):
        """Test __init__ constructor."""
        # Check default parameters.
        options = ProcessorOptions()
        self.assertEqual(options.filter_rules, [])
        self.assertIsNone(options.git_commit)
        self.assertFalse(options.is_verbose)
        self.assertEqual(options.min_confidence, 1)
        self.assertEqual(options.output_format, 'emacs')

        # Check argument validation.
        self.assertRaises(ValueError, ProcessorOptions, output_format='bad')
        ProcessorOptions(output_format='emacs')  # No ValueError: works
        ProcessorOptions(output_format='vs7')  # works
        self.assertRaises(ValueError, ProcessorOptions, min_confidence=0)
        self.assertRaises(ValueError, ProcessorOptions, min_confidence=6)
        ProcessorOptions(min_confidence=1)  # works
        ProcessorOptions(min_confidence=5)  # works

        # Check attributes.
        options = ProcessorOptions(
            filter_rules=['+'],
            git_commit='commit',
            is_verbose=True,
            min_confidence=3,
            output_format='vs7')
        self.assertEqual(options.filter_rules, ['+'])
        self.assertEqual(options.git_commit, 'commit')
        self.assertTrue(options.is_verbose)
        self.assertEqual(options.min_confidence, 3)
        self.assertEqual(options.output_format, 'vs7')

    def test_eq(self):
        """Test __eq__ equality function."""
        self.assertTrue(ProcessorOptions().__eq__(ProcessorOptions()))

        # Also verify that a difference in any argument causes equality to fail.

        # Explicitly create a ProcessorOptions instance with all default
        # values.  We do this to be sure we are assuming the right default
        # values in our self.assertFalse() calls below.
        options = ProcessorOptions(
            filter_rules=[],
            git_commit=None,
            is_verbose=False,
            min_confidence=1,
            output_format='emacs')
        # Verify that we created options correctly.
        self.assertTrue(options.__eq__(ProcessorOptions()))

        self.assertFalse(options.__eq__(ProcessorOptions(filter_rules=['+'])))
        self.assertFalse(options.__eq__(ProcessorOptions(git_commit='commit')))
        self.assertFalse(options.__eq__(ProcessorOptions(is_verbose=True)))
        self.assertFalse(options.__eq__(ProcessorOptions(min_confidence=2)))
        self.assertFalse(options.__eq__(ProcessorOptions(output_format='vs7')))

    def test_ne(self):
        """Test __ne__ inequality function."""
        # By default, __ne__ always returns true on different objects.
        # Thus, just check the distinguishing case to verify that the
        # code defines __ne__.
        self.assertFalse(ProcessorOptions().__ne__(ProcessorOptions()))
